[replicaId].tsx 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. import { useParams } from 'common'
  2. import { Database } from 'icons'
  3. import { Loader2, Trash } from 'lucide-react'
  4. import Link from 'next/link'
  5. import { useRouter } from 'next/router'
  6. import { useEffect, useMemo, useState } from 'react'
  7. import { AWS_REGIONS } from 'shared-data'
  8. import { Badge, Button } from 'ui'
  9. import { ShimmeringLoader } from 'ui-patterns'
  10. import { ReadReplicaDetails } from '@/components/interfaces/Database/Replication/ReadReplicas/ReadReplicaDetails'
  11. import {
  12. getIsInTransition,
  13. getStatusLabel,
  14. } from '@/components/interfaces/Database/Replication/ReadReplicas/ReadReplicas.utils'
  15. import { DropReplicaConfirmationModal } from '@/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/DropReplicaConfirmationModal'
  16. import { REPLICA_STATUS } from '@/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants'
  17. import { RestartReplicaConfirmationModal } from '@/components/interfaces/Settings/Infrastructure/InfrastructureConfiguration/RestartReplicaConfirmationModal'
  18. import DatabaseLayout from '@/components/layouts/DatabaseLayout/DatabaseLayout'
  19. import { DefaultLayout } from '@/components/layouts/DefaultLayout'
  20. import { PageLayout } from '@/components/layouts/PageLayout/PageLayout'
  21. import { ScaffoldDescription, ScaffoldTitle } from '@/components/layouts/Scaffold'
  22. import { ButtonTooltip } from '@/components/ui/ButtonTooltip'
  23. import CopyButton from '@/components/ui/CopyButton'
  24. import { useReadReplicasQuery } from '@/data/read-replicas/replicas-query'
  25. import {
  26. ReplicaInitializationStatus,
  27. useReadReplicasStatusesQuery,
  28. } from '@/data/read-replicas/replicas-status-query'
  29. import type { NextPageWithLayout } from '@/types'
  30. const DatabaseReadReplicaPage: NextPageWithLayout = () => {
  31. const router = useRouter()
  32. const { ref, replicaId } = useParams()
  33. const [showConfirmRestart, setShowConfirmRestart] = useState(false)
  34. const [showConfirmDrop, setShowConfirmDrop] = useState(false)
  35. const [statusRefetchInterval, setStatusRefetchInterval] = useState<number | false>(5000)
  36. const {
  37. data: databases = [],
  38. isPending: isLoadingDatabases,
  39. isSuccess: isSuccessDatabases,
  40. } = useReadReplicasQuery({ projectRef: ref })
  41. const replica = databases.find((x) => x.identifier === replicaId)
  42. const { identifier, region, status: baseStatus } = replica ?? {}
  43. const { data: statuses = [], isSuccess: isSuccessReplicasStatuses } =
  44. useReadReplicasStatusesQuery({ projectRef: ref }, { refetchInterval: statusRefetchInterval })
  45. const replicaStatus = statuses.find((x) => x.identifier === identifier)
  46. const status = replicaStatus?.status ?? baseStatus
  47. const initStatus = replicaStatus?.replicaInitializationStatus?.status
  48. const regionLabel = Object.values(AWS_REGIONS).find((x) => x.code === region)?.displayName
  49. const statusLabel = useMemo(() => getStatusLabel({ initStatus, status }), [initStatus, status])
  50. const isInTransition = useMemo(
  51. () => getIsInTransition({ initStatus, status }),
  52. [initStatus, status]
  53. )
  54. useEffect(() => {
  55. if (!isSuccessReplicasStatuses) return
  56. const pollReplicas = async () => {
  57. const fixedStatuses = [
  58. REPLICA_STATUS.ACTIVE_HEALTHY,
  59. REPLICA_STATUS.ACTIVE_UNHEALTHY,
  60. REPLICA_STATUS.INIT_READ_REPLICA_FAILED,
  61. ]
  62. const replicasInTransition = statuses.filter((db) => {
  63. const { status } = db.replicaInitializationStatus || {}
  64. return (
  65. !fixedStatuses.includes(db.status) || status === ReplicaInitializationStatus.InProgress
  66. )
  67. })
  68. const hasTransientStatus = replicasInTransition.length > 0
  69. // If all replicas are active healthy, stop fetching statuses
  70. if (!hasTransientStatus && statuses.length === databases.length) {
  71. setStatusRefetchInterval(false)
  72. }
  73. }
  74. pollReplicas()
  75. }, [databases.length, isSuccessReplicasStatuses, statuses])
  76. return (
  77. <PageLayout
  78. title={
  79. <div className="flex items-center gap-x-3">
  80. <ScaffoldTitle>Read Replica</ScaffoldTitle>
  81. {isSuccessDatabases && (
  82. <>
  83. <Badge
  84. variant={
  85. statusLabel === 'Healthy'
  86. ? 'success'
  87. : statusLabel === 'Failed'
  88. ? 'destructive'
  89. : 'default'
  90. }
  91. >
  92. {statusLabel}
  93. </Badge>
  94. {isInTransition && <Loader2 size={14} className="animate-spin" />}
  95. </>
  96. )}
  97. </div>
  98. }
  99. subtitle={
  100. isLoadingDatabases ? (
  101. <ShimmeringLoader className="py-[11px]" />
  102. ) : (
  103. <div className="flex items-center gap-x-2 mt-0!">
  104. <ScaffoldDescription>ID: {identifier}</ScaffoldDescription>
  105. <CopyButton iconOnly type="default" text={identifier ?? ''} />
  106. </div>
  107. )
  108. }
  109. icon={
  110. <div className="shrink-0 w-10 h-10 relative bg-surface-100 border rounded-md flex items-center justify-center">
  111. <Database size={20} className="text-foreground-light" />
  112. </div>
  113. }
  114. breadcrumbs={[
  115. {
  116. label: 'Replication',
  117. href: `/project/${ref}/database/replication`,
  118. },
  119. {
  120. label: `Read Replica - ${regionLabel}`,
  121. },
  122. ]}
  123. secondaryActions={
  124. <ButtonTooltip
  125. type="default"
  126. className="w-7"
  127. icon={<Trash />}
  128. tooltip={{
  129. content: { side: 'bottom', text: 'Drop replica' },
  130. }}
  131. onClick={() => setShowConfirmDrop(true)}
  132. />
  133. }
  134. primaryActions={[
  135. <Button asChild key="logs" type="default">
  136. <Link
  137. href={`/project/${ref}/logs/postgres-logs${!!identifier ? `?db=${identifier}` : ''}`}
  138. >
  139. View logs
  140. </Link>
  141. </Button>,
  142. <Button
  143. key="drop"
  144. type="default"
  145. disabled={status !== 'ACTIVE_HEALTHY'}
  146. onClick={() => setShowConfirmRestart(true)}
  147. >
  148. Restart replica
  149. </Button>,
  150. ]}
  151. >
  152. <ReadReplicaDetails />
  153. <DropReplicaConfirmationModal
  154. selectedReplica={showConfirmDrop ? replica : undefined}
  155. onSuccess={() => router.push(`/project/${ref}/database/replication`)}
  156. onCancel={() => setShowConfirmDrop(false)}
  157. />
  158. <RestartReplicaConfirmationModal
  159. selectedReplica={showConfirmRestart ? replica : undefined}
  160. onSuccess={() => setStatusRefetchInterval(5000)}
  161. onCancel={() => setShowConfirmRestart(false)}
  162. />
  163. </PageLayout>
  164. )
  165. }
  166. DatabaseReadReplicaPage.getLayout = (page) => (
  167. <DefaultLayout>
  168. <DatabaseLayout title="Replication">{page}</DatabaseLayout>
  169. </DefaultLayout>
  170. )
  171. export default DatabaseReadReplicaPage